﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.ServiceModel;
using System.Windows.Forms;
using Microsoft.Xrm.Sdk.Client;
using Microsoft.Xrm.Sdk.Metadata;
using Microsoft.Xrm.Sdk.Messages;
using System.Runtime.CompilerServices;

namespace Project.Co_DataTransformationPrototype
{
    public class CRMModelBuilder : ModelBuilder
    {
        // Declarations
        private static CRMModelBuilder instance;
        private RetrieveAllEntitiesResponse crmResponse;

        private Hashtable _excludedRelationsTable = new Hashtable();
        private string[] _excludedRelations = new string[] { "owningteam", "organizationid" };
        private Dictionary<int, Entity> _proccessedEntity = new Dictionary<int, Entity>();

        public OrganizationServiceProxy serviceProxy;
        public Dictionary<int, EntityMetadata> CRMEntityCollection { get; set; }

        private CRMModelBuilder(ModelRoot modelRoot)
        {
            // TODO: receive CRM server configuration properties from a win-form
            this.objModelRoot = modelRoot;
            // Only re-connect if previous response has been nullified
            if (crmResponse == null)
                connectToCRMServer();
        }

        public static CRMModelBuilder GetInstance(ModelRoot modelRoot)
        {
            if (instance == null)
                instance = new CRMModelBuilder(modelRoot);
            else if (instance.objModelRoot.Store == null)
                instance = new CRMModelBuilder(modelRoot);

            return instance;
        }

        public override void BuildDiagram(string mainEntityName)
        {
            if (crmResponse == null)
                return;
            
            CRMEntityCollection = inputCleanUp(crmResponse);

            // Get the metadata for each passed-in entity, draw it, and draw its relationships.
            foreach (EntityMetadata crmEntity in CRMEntityCollection.Values)
            {
                if (crmEntity.LogicalName != mainEntityName)  // TODO: This is a temp filtering (keeping import short)
                    continue;

                // Draw all relationshipos FROM this entity
                createAllRelatedEntities(crmEntity, crmEntity.OneToManyRelationships, true);

                // Draw entity and all relationships TO this entity
                createAllRelatedEntities(crmEntity, crmEntity.ManyToManyRelationships, false);
                createAllRelatedEntities(crmEntity, crmEntity.ManyToOneRelationships, false);
            }
            MessageBox.Show("Model building for " + mainEntityName + " completed.", "CRM ModelBuilder - Finished", MessageBoxButtons.OK, MessageBoxIcon.Information);

            // Reset
            _excludedRelationsTable.Clear();
            _proccessedEntity.Clear();
        }

        /// <summary>
        /// Create a element for a CRM Entity, and add it to the diagram
        /// </summary>
        /// <param name="entity"></param>
        /// <param name="ownership"></param>
        public override Guid createEntity<T>(T entityObject, bool hasForeignKey, string fkAttributeName, Guid referencedEntityId)
        {
            // Define entity
            EntityMetadata entity = (entityObject as EntityMetadata);
            EntityInfo eInfo = new EntityInfo(objModelRoot, entity.LogicalName, true);

            // Define its attributes
            foreach (AttributeMetadata attribute in entity.Attributes)
            {
                AttributeInfo attrInfo = new AttributeInfo();
                attrInfo.name = attribute.LogicalName;
                attrInfo.type = MappingTypeConvertion.translateCRMDataTypeIntoSQLDbType(attribute.AttributeType.Value);
                attrInfo.typeDescription = attribute.AttributeType.Value.ToString();
                attrInfo.isPrimaryKey = attribute.IsPrimaryId.Value;
                attrInfo.isForeignKey = (hasForeignKey && (fkAttributeName != null && string.Equals(attribute.LogicalName, fkAttributeName)) ? true : false);
                if (attrInfo.isForeignKey)
                    attrInfo.referencedEntity = referencedEntityId;

                eInfo.attributeList.Add(attrInfo);
            }

            // Add Entity & its attributes
            System.Guid newElementID = System.Guid.Empty;
            Operators.Define(objModelRoot.Store).CreateOperator(new List<EntityInfo> { eInfo }, out newElementID);

            if (newElementID != System.Guid.Empty)
            {
                Entity ent = objModelRoot.Store.ElementDirectory.FindElement(newElementID) as Entity;
                if (ent != null)
                    _proccessedEntity.Add(ent.Name.GetHashCode(), ent);
                else
                    throw new NotImplementedException("Error, cannot find created element or, element was not created succesfully");
            }
            else
            {
                throw new NotImplementedException("Error, cannot find created element or, element was not created succesfully");
            }

            return newElementID;
        }

        //private System.Guid createEntity(EntityMetadata entity, bool isForeignKeyKnown, string nameFkAttribute = null, System.Guid? referencedEntityId = null)
        //{
        //    // Define entity
        //    EntityInfo eInfo = new EntityInfo(objModelRoot, entity.LogicalName, true) ;
            
        //    // Define its attributes
        //    foreach (AttributeMetadata attribute in entity.Attributes)
        //    {
        //        AttributeInfo attrInfo = new AttributeInfo();
        //        attrInfo.name = attribute.LogicalName;
        //        attrInfo.type = MappingTypeConvertion.translateCRMDataTypeIntoSQLDbType(attribute.AttributeType.Value);
        //        attrInfo.typeDescription = attribute.AttributeType.Value.ToString();
        //        attrInfo.isPrimaryKey = attribute.IsPrimaryId.Value;
        //        attrInfo.isForeignKey = (isForeignKeyKnown && (nameFkAttribute != null && string.Equals(attribute.LogicalName, nameFkAttribute)) ? true : false);
        //        if (attrInfo.isForeignKey)
        //            attrInfo.referencedEntity = referencedEntityId.Value;

        //        eInfo.attributeList.Add(attrInfo);
        //    }

        //    // Add Entity & its attributes
        //    System.Guid newElementID = System.Guid.Empty;
        //    Operators.Define(objModelRoot.Store).CreateOperator(new List<EntityInfo> { eInfo }, out newElementID);

        //    if (newElementID != System.Guid.Empty)
        //    {
        //        Entity ent = objModelRoot.Store.ElementDirectory.FindElement(newElementID) as Entity;
        //        if (ent != null)
        //            _proccessedEntity.Add(ent.Name.GetHashCode(), ent);
        //        else
        //            throw new NotImplementedException("Error, cannot find created element or, element was not created succesfully");
        //    }
        //    else
        //    {
        //        throw new NotImplementedException("Error, cannot find created element or, element was not created succesfully");
        //    }

        //    return newElementID;
        //}

        /// <summary>
        /// Create a link between two model elements.
        /// </summary>
        /// <param name="elementA"></param>
        /// <param name="elementB"></param>
        public override void createLink(Entity elementA, Entity elementB, string nameAttributeInA, string nameAtttributeInB)
        {
            ConnectorInfo connectorInfo = new ConnectorInfo();
            connectorInfo.source = elementA;
            connectorInfo.nameAttributeInSource = nameAttributeInA;
            connectorInfo.target = elementB;
            connectorInfo.nameAttributeInTarget = nameAtttributeInB;

            Operators.Define(elementA.Store).CreateOperator(new List<ConnectorInfo> { connectorInfo });
        }

        /// <summary>
        /// Create an element for each relationship in the element's relationship collection
        /// </summary>
        /// <param name="entity"></param>
        /// <param name="relationshipCollection"></param>
        /// <param name="areReferencingRelationships"></param>
        private void createAllRelatedEntities(EntityMetadata entity, RelationshipMetadataBase[] relationshipCollection, bool areReferencingRelationships)
        {
            ManyToManyRelationshipMetadata currentManyToManyRelationship = null;
            OneToManyRelationshipMetadata currentOneToManyRelationship = null;
            ArrayList _processedRelationships = new ArrayList(128);
            EntityMetadata entity2 = null;
            AttributeMetadata attribute2 = null;
            AttributeMetadata attribute = null;
            Guid metadataID = Guid.NewGuid();
            bool isManyToMany = false;

            foreach (RelationshipMetadataBase entityRelationship in relationshipCollection)
            {
                entity2 = null;

                if (entityRelationship is ManyToManyRelationshipMetadata)
                {
                    isManyToMany = true;
                    currentManyToManyRelationship = entityRelationship as ManyToManyRelationshipMetadata;
                    // The entity passed in is not necessarily the originator of this relationship.
                    if (String.Compare(entity.LogicalName, currentManyToManyRelationship.Entity1LogicalName, true) != 0)
                    {
                        entity2 = (EntityMetadata)CRMEntityCollection[currentManyToManyRelationship.Entity1LogicalName.GetHashCode()];
                    }
                    else
                    {
                        entity2 = (EntityMetadata)CRMEntityCollection[currentManyToManyRelationship.Entity2LogicalName.GetHashCode()];
                    }
                    attribute2 = getAttributeMetadata(entity2, entity2.PrimaryIdAttribute);
                    attribute = getAttributeMetadata(entity, entity.PrimaryIdAttribute);
                    metadataID = currentManyToManyRelationship.MetadataId.Value;
                }
                else if (entityRelationship is OneToManyRelationshipMetadata)
                {
                    isManyToMany = false;
                    currentOneToManyRelationship = entityRelationship as OneToManyRelationshipMetadata;
                    entity2 = (EntityMetadata)CRMEntityCollection[(areReferencingRelationships ? currentOneToManyRelationship.ReferencingEntity.GetHashCode() : currentOneToManyRelationship.ReferencedEntity.GetHashCode())];
                    attribute2 = getAttributeMetadata(entity2, areReferencingRelationships ? currentOneToManyRelationship.ReferencingAttribute : currentOneToManyRelationship.ReferencedAttribute);
                    attribute = getAttributeMetadata(entity, areReferencingRelationships ? currentOneToManyRelationship.ReferencedAttribute : currentOneToManyRelationship.ReferencingAttribute);
                    metadataID = currentOneToManyRelationship.MetadataId.Value;
                }
                // Verify relationship is either ManyToManyMetadata or OneToManyMetadata
                if (entity2 != null)
                {
                    if (_processedRelationships.Contains(metadataID))
                    {
                        // Skip relationships we have already processed
                        continue;
                    }
                    else
                    {
                        // Record that we are processing this relationship
                        _processedRelationships.Add(metadataID);

                        // Do not create relationships involving the entity itself, SystemUser, BusinessUnit,
                        // or those that are intentionally excluded.
                        if (String.Compare(entity2.LogicalName, "systemuser", true) != 0 &&
                            String.Compare(entity2.LogicalName, "businessunit", true) != 0 &&
                            String.Compare(entity2.LogicalName, entity.LogicalName, true) != 0 &&
                            String.Compare(entity.LogicalName, "systemuser", true) != 0 &&
                            String.Compare(entity.LogicalName, "businessunit", true) != 0 &&
                       !_excludedRelationsTable.ContainsKey(attribute.LogicalName.GetHashCode()))
                        {
                            // Create a link between the two entity objects
                            if (areReferencingRelationships)
                                addRelationship(entity, entity2, attribute.LogicalName, attribute2.LogicalName, isManyToMany);
                            else
                                addRelationship(entity2, entity, attribute2.LogicalName, attribute.LogicalName, isManyToMany);
                        }
                        //else
                        //{
                        //    int dummy = 0; //System.IO. .WriteLine(String.Format("<{0} - {1}> not drawn.", rect.Name, entity2.LogicalName), "Relationship");
                        //}
                    }
                }
            }
        }

        private void addRelationship(EntityMetadata entityFrom, EntityMetadata entityTo, string nameSourceAttribute, string nameTargetAttribute, bool isManyToMany)
        {
            System.Guid sourceElementID = System.Guid.Empty;
            if (!_proccessedEntity.ContainsKey(entityFrom.LogicalName.GetHashCode()))
                sourceElementID = createEntity(entityFrom, false, null, Guid.Empty);
            else
                sourceElementID = (objModelRoot.Store.ElementDirectory.FindElements<EntityShape>().FirstOrDefault<EntityShape>(s => string.Equals((s.ModelElement as Entity).CurrentName, entityFrom.LogicalName)).ModelElement as Entity).Id;

            if (!_proccessedEntity.ContainsKey(entityTo.LogicalName.GetHashCode()))
                createEntity(entityTo, true, nameTargetAttribute, sourceElementID);

            createLink(_proccessedEntity[entityFrom.LogicalName.GetHashCode()], _proccessedEntity[entityTo.LogicalName.GetHashCode()], nameSourceAttribute, nameTargetAttribute);
        }

        /// <summary>
        /// Connect to CRM Server
        /// </summary>
        /// <returns></returns>
        private void connectToCRMServer()
        {
            StringBuilder errorString = new StringBuilder();
            string msgBoxConnectedCaption = "CRM ModelBuilder - Connected";
            string msgBoxErrorCaption = "CRM ModelBuilder - Error";
            string msgBoxAbortedCaption = "CRM ModelBuilder - Aborted";
            
            try
            {
                // Obtain the target organization's Web address and client logon 
                // credentials from the user.
                ServerConnection serverConnect = new ServerConnection();
                ServerConnection.Configuration config = serverConnect.GetServerConfiguration();

                if (config != null)
                {
                    // Connect to the Organization service
                    // The using statement assures that the service proxy will be properly disposed
                    using (serviceProxy = ServerConnection.GetOrganizationProxy(config))
                    {
                        // This statement is required to enable early-bound type support
                        serviceProxy.EnableProxyTypes();

                        // Load the metadata
                        RetrieveAllEntitiesRequest request = new RetrieveAllEntitiesRequest()
                        {
                            EntityFilters = EntityFilters.Entity | EntityFilters.Attributes | EntityFilters.Relationships,
                            RetrieveAsIfPublished = true
                        };
                        crmResponse = (RetrieveAllEntitiesResponse)serviceProxy.Execute(request);
                    }
                    MessageBox.Show("Connection attempt completed.", msgBoxConnectedCaption, MessageBoxButtons.OK, MessageBoxIcon.Information);
                }
                else
                {
                    MessageBox.Show("Connection to CRM Server was aborted.", msgBoxAbortedCaption, MessageBoxButtons.OK, MessageBoxIcon.Stop);
                }
            }
            catch (FaultException<Microsoft.Xrm.Sdk.OrganizationServiceFault> ex)
            {
                errorString.AppendFormat("Timestamp: {0}", ex.Detail.Timestamp);
                errorString.AppendFormat("Code: {0}", ex.Detail.ErrorCode);
                errorString.AppendFormat("Message: {0}", ex.Detail.Message);
                errorString.AppendFormat("Plugin Trace: {0}", ex.Detail.TraceText);
                errorString.AppendFormat("Inner Fault: {0}",
                    null == ex.Detail.InnerFault ? "Has Inner Fault" :
                    "No Inner Fault");
                MessageBox.Show(errorString.ToString(), msgBoxErrorCaption, MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            catch (System.TimeoutException ex)
            {
                errorString.AppendFormat("Message: {0}", ex.Message);
                errorString.AppendFormat("Stack Trace: {0}", ex.StackTrace);
                errorString.AppendFormat("Inner Fault: {0}",
                    null == ex.InnerException.Message ? "Has Inner Fault" :
                    "No Inner Fault");
                MessageBox.Show(errorString.ToString(), msgBoxErrorCaption, MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            catch (System.Exception ex)
            {
                MessageBox.Show(ex.Message, msgBoxErrorCaption, MessageBoxButtons.OK, MessageBoxIcon.Error);

                // Display the details of the inner exception.
                if (ex.InnerException != null)
                {
                    MessageBox.Show(ex.InnerException.Message, msgBoxErrorCaption, MessageBoxButtons.OK, MessageBoxIcon.Information);

                    FaultException<Microsoft.Xrm.Sdk.OrganizationServiceFault> fe
                        = ex.InnerException
                        as FaultException<Microsoft.Xrm.Sdk.OrganizationServiceFault>;
                    if (fe != null)
                    {
                        errorString.AppendFormat("Timestamp: {0}", fe.Detail.Timestamp);
                        errorString.AppendFormat("Code: {0}", fe.Detail.ErrorCode);
                        errorString.AppendFormat("Message: {0}", fe.Detail.Message);
                        errorString.AppendFormat("Plugin Trace: {0}", fe.Detail.TraceText);
                        errorString.AppendFormat("Inner Fault: {0}",
                            null == fe.Detail.InnerFault ? "Has Inner Fault" :
                            "No Inner Fault");
                        MessageBox.Show(errorString.ToString(), msgBoxErrorCaption);
                    }
                }
            }
            finally
            {
                if (crmResponse == null)
                    instance = null;
            }
            // Additonal exceptions to catch: SecurityTokenValidationException, ExpiredSecurityTokenException,
            // SecurityAccessDeniedException, MessageSecurityException, and SecurityNegotiationException.
        }

        /// <summary>
        /// Exclude entities that exist in the metadata but are not to be drawn in the diagram
        /// </summary>
        /// <param name="entityCollection"></param>
        /// <returns></returns>
        private Dictionary<int, EntityMetadata> inputCleanUp(RetrieveAllEntitiesResponse entityCollection)
        {
            Dictionary<int, EntityMetadata> cleanedList = new Dictionary<int, EntityMetadata>();

            // These entities exist in the metadata but are not to be drawn in the diagram.
            Hashtable _excludedEntityTable = new Hashtable();
            string[] _excludedEntities = new string[] {
				    "attributemap", "bulkimport", "businessunitmap",
				    "commitment", "displaystringmap", "documentindex",
				    "entitymap", "importconfig", "integrationstatus",
				    "internaladdress", "privilegeobjecttypecodes", 
				    "roletemplate", "roletemplateprivileges", 
				    "statusmap", "stringmap", "stringmapbit" };

            // Build a hashtable from the array of excluded entities. This will
            // allow for faster lookups when determining if an entity is to be excluded.
            for (int n = 0; n < _excludedEntities.Length; n++)
                _excludedEntityTable.Add(_excludedEntities[n].GetHashCode(), _excludedEntities[n]);

            // Do the same for excluded relationships.
            for (int n = 0; n < _excludedRelations.Length; n++)
            {
                if (!_excludedRelationsTable.ContainsKey(_excludedRelations[n].GetHashCode()))
                    _excludedRelationsTable.Add(_excludedRelations[n].GetHashCode(), _excludedRelations[n]);
            }

            // Only draw an entity if it does not exist in the excluded entity table.
            foreach (EntityMetadata entity in entityCollection.EntityMetadata)
            {
                if (!_excludedEntityTable.ContainsKey(entity.LogicalName.GetHashCode()))
                    cleanedList.Add(entity.LogicalName.GetHashCode(), entity);
            }

            return cleanedList;
        }
        
        /// <summary>
        /// 
        /// </summary>
        /// <param name="entity"></param>
        /// <param name="attributeName"></param>
        /// <returns></returns>
        private AttributeMetadata getAttributeMetadata(EntityMetadata entity, string attributeName)
        {
            AttributeMetadata attrmdCurrent = null;
            foreach (AttributeMetadata attrib in entity.Attributes)
            {
                if (attrib.LogicalName == attributeName)
                {
                    attrmdCurrent = attrib;
                    break;
                }
            }
            return attrmdCurrent;
        }

    }
}
